#include "vsLogic.h"
#include "io/vsMedia.h"
#include "io/vsFileOps.h"

#include <QFileSystemWatcher>
#include <QFileInfo>
#include <QSettings>
#include <QApplication>
#include <QValidator>
#include <QTimer>
#include <QUrl>
#include <QDir>
#include <QBuffer>


vsUI::vsUI(QObject *other)
    : QObject(other), initialized(false)
{
    //
}

vsUI::~vsUI()
{
    //
}

//----------------------------------

vsLogic::vsLogic(QObject *parent)
    : QObject(parent), vsInfoController()
{
    QSettings s(c_def_appsettings_init);

    network = new vsServer(this, this);
    network->startAtPort(s.value("server_port", c_default_network_port).toInt());
    connect(network, SIGNAL(transmitted(QString,qint64)), this, SLOT(onNetworkInfo(QString,qint64)));

    network->onURLChanged(s.value("my_url", QString("http://localhost:%1/").arg(c_default_network_port)).toString());

    QTimer::singleShot(1000, this, SLOT(netInfoTimer()));

    fsWatchTimer = new QTimer(this);
    fsWatchTimer->setInterval(s.value("folder_scan_interval", c_folder_scan_interval).toInt() * 1000);
    connect(fsWatchTimer, &QTimer::timeout, this, &vsLogic::folderWatchTimer);

    fsWatcher = new QFileSystemWatcher(this);
    connect(fsWatcher, &QFileSystemWatcher::fileChanged, this, &vsLogic::onWatchedFileChanged);

    QString prevFiles = s.value("files").toString();

    if (!prevFiles.isEmpty())
    {
        QStringList pairs = prevFiles.split("%%%");

        foreach (QString p, pairs)
        {
            QStringList parts = p.split("&&&");

            if (parts.count() == 2)
                onAddFile(parts.first(), QString("id=%1").arg(parts.last()));
        }
    }

    QString prevWatchedFolders = s.value("watched_folders").toString();

    if (!prevWatchedFolders.isEmpty())
        foreach (const QString &wf, prevWatchedFolders.split("%%%"))
            onWatchFolder(wf);

    if (!watchedFolders.isEmpty())
        fsWatchTimer->start();
}

vsLogic::~vsLogic()
{
    network->stop();
    QSettings s(c_def_appsettings_init);
    s.setValue("server_port", network->port());

    qDebug() << "Logic destroyed";
}

//-----------------------------------

bool vsLogic::connectUI(vsUI *ui)
{
    bool result = false;

    if (ui != 0)
    {
        connect(ui, SIGNAL(addFile(QString,QString)), this, SLOT(onAddFile(QString,QString)));
        connect(this, SIGNAL(fileAdded(vsMediaFile*)), ui, SLOT(onFileAdded(vsMediaFile*)));
        connect(ui, SIGNAL(exiting()), this, SLOT(onExit()));

        connect(this, SIGNAL(networkInfoUpdated(QString,int,int,int,qint64)),
                ui, SLOT(onNetworkInfoUpdated(QString,int,int,int,qint64)));

        connect(ui, SIGNAL(portChanged(int)), this, SLOT(onPortChange(int)));

        connect(ui, SIGNAL(mediaIDChanged(QString,QString)), this, SLOT(onMediaIDChanged(QString,QString)));
        connect(ui, SIGNAL(removeMedia(QString)), this, SLOT(onRemoveMedia(QString)));

        connect(this, SIGNAL(mediaIDChanged(QString,vsMediaFile*)), ui, SLOT(onMediaIDChanged(QString,vsMediaFile*)));
        connect(this, SIGNAL(mediaRemoved(QString)), ui, SLOT(onFileRemoved(QString)));

        // folder watching ------------
        connect(ui, SIGNAL(watchedFolderAdded  (QString)), this, SLOT(onWatchFolder       (QString)));
        connect(ui, SIGNAL(watchedFolderRemoved(QString)), this, SLOT(onStopWatchingFolder(QString)));

        connect(this, SIGNAL(watchingFolder       (QString)), ui, SLOT(watchFolder       (QString)));
        connect(this, SIGNAL(stoppedWatchingFolder(QString)), ui, SLOT(stopWatchingFolder(QString)));

        connect(ui, &vsUI::fwScanIntervalChanged, [this](const int &newInt){ fsWatchTimer->setInterval(newInt * 1000); });
        //-----------------------------

        connect(ui, SIGNAL(urlEdited(QString)), this, SLOT(onURLEdited(QString)));

        ui->onConnected();

        foreach (vsMediaFile *mf, files)
            emit fileAdded(mf);

        foreach (const QString &wf, watchedFolders.keys())
            emit watchingFolder(wf);
    }

    return result;
}

bool vsLogic::canServeID(QString id)
{
    if (id.isEmpty() || !filesMap.contains(id))
        return false;

    vsMediaFile *mf = filesMap[id];
    assert(mf != 0);

    QFileInfo fi(mf->path);

    if (!(fi.exists() && fi.isFile() && fi.isReadable()))
    {
        vsLog::e(tr("File not available anymore: ") + mf->path);
        onRemoveMedia(mf->id);
        return false;
    }
    else
        return mf->status == vsTypes::seeding;
}

bool vsLogic::getMediaInfo(vsNetworkMediaInfo &i)
{
    if (!canServeID(i.id))
        return false;

    vsMediaFile *mf = filesMap[i.id];
    assert(mf != 0);

    i.path     = mf->path;
    i.mime     = mf->mime;
    i.size     = mf->size;
    i.duration = mf->duration;
    i.tags     = mf->tags;

    if (!mf->covers.isEmpty())
    {
        if (mf->cachedCover.isEmpty())
        {
            QBuffer b(&mf->cachedCover);
            b.open(QIODevice::WriteOnly);

            if (b.isOpen())
            {
                const QImage &firstPoster = mf->covers.first();
                firstPoster.save(&b, "png");
                mf->cachedCoverSize = firstPoster.size();
                mf->cachedCoverMime = "image/png";

                b.close();
            }
        }

        i.poster     = mf->cachedCover;
        i.posterSize = mf->cachedCoverSize;
        i.posterMime = mf->cachedCoverMime;

    }

    i.tags["filename"] = QUrl::toPercentEncoding(QFileInfo(mf->path).fileName());

    netstats[i.id].requests++;

    return true;
}

//--------- slots -------------------------------
void vsLogic::onAppMessage(const QString &msg)
{
    vsLog::i(QString("IPC: %1").arg((msg == "vocaseed_request") ? "VocaSeed is already running." : msg));
}

void vsLogic::onAddFile(const QString &path, QString params)
{
    if (filepathsMap.contains(path))
        return;

    QFileInfo fi(path);

    if (fi.exists() && fi.isFile() && fi.isReadable())
    {
        // determine mime
        vsMediaTypeSettings s;

        if (vsMedia::instance()->supports(fi.suffix(), &s))
        {
            QStringList paramList = params.split(' ');

            vsMediaFile *mf = new vsMediaFile(path, this);
            mf->mime = s.mime;
            mf->name = fi.fileName();
            mf->ext  = fi.suffix();

            // hack because QFileInfo.baseName() returns left part until first point
            //  for "01. coolname.mp3" that will be "01"
            if (!mf->ext.isEmpty())
                mf->name = mf->name.left(mf->name.length() - mf->ext.length() - 1); // 1 for point

            mf->size = fi.size();
            bool idOK = false;
            int tries = 0;
            QString newID;

            foreach (QString p, paramList)
            {
                if (p.contains("id="))
                {
                    p.remove("id=");

                    QRegExp rx(QString("[0-9a-z_]{1,%1}").arg(c_vctb_id_max_length));

                    if (rx.exactMatch(p) && !filesMap.contains(p))
                    {
                        newID = p;
                        break;
                    }
                }
            }

            if (newID.isEmpty())
            {
                // try to make an id from name
                newID = mf->name.toLower().replace(' ', '_').remove('\'').replace('.', '_')
                                .remove(QRegExp("[^0-9a-z_]"));

                if (newID.length() > c_vctb_id_max_length)
                    newID.resize(c_vctb_id_max_length);

                idOK = !(newID.isEmpty() || filesMap.contains(newID));

                while (!idOK && tries <= 9000)
                {
                    tries++;
                    newID = generateLocalID();
                    idOK = !filesMap.contains(newID);
                }
            }

            if (!idOK && tries > 9000)
            {
                vsLog::e("Oh noes, something's totally wrong with ID generation!");
                //QApplication::exit(-9001); // hmhmhm maybe yes maybe no
                vsLog::e("This is really, really bad. Like, totally absolutely the baddest thing that could ever happen with this app.");
            }

            mf->id = newID;
            mf->status = vsTypes::checking;

            if (!vsMedia::instance()->requestMediaInfo(mf))
                qDebug() << "Requesting media info failed!";

            files.append(mf);
            filesMap[mf->id] = mf;
            filepathsMap[mf->path] = mf;

            fsWatcher->addPath(mf->path);

            emit fileAdded(mf);
        }
    }
}

void vsLogic::onExit()
{
    QSettings s(c_def_appsettings_init);

    // TODO: gracefully terminate all file and network operations
    vsUI *ui = qobject_cast<vsUI*>(sender());

    if (ui)
    {
        QStringList sortedIDList = ui->getIdOrder();
        QString medias;

        foreach (const QString &id, sortedIDList)
        {
            vsMediaFile *f = filesMap[id];

            if (f != 0)
                medias.append(f->path + "&&&" + f->id + "%%%");
        }

        s.setValue("files", medias); // saving even if it's empty, in case it wasn't before
    }

    // saving watched folders, even if none (in case there were before)
    QString wfSaveStr;

    foreach (const QString &wfS, watchedFolders.keys())
        wfSaveStr.append(wfS + "%%%");

    if (!wfSaveStr.isEmpty())
        wfSaveStr.chop(3);

    s.setValue("watched_folders", wfSaveStr);
    s.setValue("folder_scan_interval", fsWatchTimer->interval() / 1000);

    qApp->exit(0);
}

QString vsLogic::generateLocalID()
{
    static int n = 0;
    return QString("1lc%1").arg(n++);
}

void vsLogic::onNetworkInfo(QString path, qint64 bytesSent)
{
    if (bytesSent == c_clear_byterate_stats) // signal to clear byterate stats, transfer has stopped
    {
        vsFileNetworkStats &stats = netstats[path];

        stats.active = false;
        stats.bps.append(0);
        stats.bps.append(0);
        stats.bps.append(0);
        stats.bps.minimize();

        emit networkInfoUpdated(path, stats.requests, 0, 0, stats.totalSeeded);
    }
    else if (bytesSent > 0)
    {
        // if doesn't contain, will be constructed at this call
        vsFileNetworkStats &stats = netstats[path];

        stats.active = bytesSent > 0;
        stats.totalSeeded += bytesSent;
        stats.bps.add(bytesSent);
    }
    else
        vsLog::d(QString("vsLogic onNetworkInfo: message about %1 bytes sent, something's wrong somewhere")
                 .arg(bytesSent));
}

void vsLogic::netInfoTimer()
{
    QMap<QString, vsFileNetworkStats>::iterator k = netstats.begin();

    for (; k != netstats.end(); ++k)
    {
        vsFileNetworkStats &stats = k.value();

        if (stats.active)
        {
            stats.uprate = stats.bps.rate();
            stats.active = stats.uprate > 0;

            if (!stats.active)
                stats.bps.minimize();

            stats.bps.append(0);

            emit networkInfoUpdated(k.key(), stats.requests, stats.uprate, stats.dlrate, stats.totalSeeded);
        }
    }

    QTimer::singleShot(1000, this, SLOT(netInfoTimer()));
}

void vsLogic::onPortChange(int newPort)
{
    if (newPort >= 20 && newPort <= 64000 && newPort != network->port())
    {
        network->stop();
        network->startAtPort(newPort);
    }
}

void vsLogic::onMediaIDChanged(QString oldID, QString newID)
{
    if (filesMap.contains(oldID) && !filesMap.contains(newID))
    {
        vsMediaFile *mf = filesMap[oldID];
        mf->id = newID;

        filesMap[newID] = filesMap[oldID];
        filesMap.remove(oldID);

        emit mediaIDChanged(oldID, mf);
    }
}

void vsLogic::onRemoveMedia(QString id)
{
    if (filesMap.contains(id))
    {
        vsMediaFile *mf = filesMap[id];
        filepathsMap.remove(mf->path);
        fsWatcher->removePath(mf->path);
        filesMap.remove(id);

        files.removeAll(mf);
        delete mf;

        emit mediaRemoved(id);
    }
}

void vsLogic::onURLEdited(QString newURL)
{
    network->onURLChanged(newURL);
    // TODO: maybe notify gui on logical change, as usual?
}

void vsLogic::onWatchFolder(const QString &path)
{
    if (!watchedFolders.contains(path))
    {
        QFileInfo fi(path);

        if (fi.exists() && fi.isDir())
        {
            if (!fsWatchTimer->isActive())
                fsWatchTimer->start();

            watchedFolders[path] = true;
            fsWatcher->addPath(path);
            addMediasFromPath(path);

            emit watchingFolder(path);
        }
        else vsLog::e("Trying to add non-existent path or a file as watched folder " + path);
    }
    else vsLog::e("Trying to watch an already watched folder " + path);
}

void vsLogic::onStopWatchingFolder(const QString &path)
{
    if (watchedFolders.contains(path))
    {
        fsWatcher->removePath(path);
        watchedFolders.remove(path);

        emit stoppedWatchingFolder(path);

        if (watchedFolders.isEmpty() && fsWatchTimer->isActive())
            fsWatchTimer->stop();
    }
    else vsLog::e("Trying to stop watching a non-watched folder " + path);
}

void vsLogic::folderWatchTimer()
{
    foreach (const QString &path, watchedFolders.keys())
        addMediasFromPath(path);
}

void vsLogic::addMediasFromPath(const QString &path)
{
    QDir d(path);
    d.setFilter(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Readable);

    if (d.exists())
        foreach (const QFileInfo &i, d.entryInfoList())
            if (i.isFile())
                onAddFile(i.absoluteFilePath(), "");
            else if (i.isDir())
                addMediasFromPath(i.absoluteFilePath());
}

void vsLogic::onWatchedFileChanged(QString path)
{
    if (watchedFolders.contains(path))
    {
        vsLog::e(tr("Watched folder disappeared") + ": " + path);
        onStopWatchingFolder(path);
    }
    else if (filepathsMap.contains(path))
    {
        onRemoveMedia(filepathsMap[path]->id);
        QFileInfo fi(path);

        if (fi.exists() && fi.isFile() && fi.isReadable())
        {
            vsLog::d(tr("Media file changed, trying to reload ") + path);
            onAddFile(path, "");
        }
        else
            vsLog::e(tr("Media file disappeared") + ": " + path);
    }
    else
        vsLog::e("FS Watcher reported missing path, but VocaSeed doesn't track it: " + path);
}
